Skip to content

fix(lint): report an empty measure selection on every widget family, not only charts - #15668

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-15508-widget-measures-missing-all-families
Sep 5, 2026
Merged

fix(lint): report an empty measure selection on every widget family, not only charts#15668
baozhoutao merged 1 commit into
mainfrom
claude/issue-15508-widget-measures-missing-all-families

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15508

The ruling, and what landed

chart-measures-missing (#15462) reported the authoring placeholder for the chart family
only. The return that produces it is type-independent. Read at the revision this repo pins
(.objectui-sha = a472b07167a39e55491109e864bb5a54027dcfbd), in
packages/plugin-dashboard/src/DatasetWidget.tsx:

  • :683if (values.length === 0) returns
    tt('dashboard.pickMeasures', 'Pick measures (values) for this dataset widget.');
  • :343const METRIC_TYPES = new Set(['metric', 'kpi', 'gauge', 'solid-gauge', 'bullet']);
  • :423const isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0;
  • :424const isTable = widgetType === 'table' || widgetType === 'pivot';

:683 stands ABOVE :423/:424 and above the chart branch, and tests nothing but
values.length === 0. Premise confirmed at the pinned sha (the card quoted an older
.objectui-sha; the four lines read identically at a472b0716). So a metric, kpi,
gauge, solid-gauge, bullet, table or pivot widget selecting no measures renders
the same placeholder — the KPI number or the table is not drawn at all — and nothing
reported it (table-count-only needs values.length > 0 before it looks; the rules that
iterate dimensions[]/values[] are silent on an empty array by construction).

The measures-missing POPULATION is now every declared ChartTypeSchema widget type. Same
warning tier, same per-widget suppressWarnings suppression, same advises-never-gates
property. chart-dimensions-missing stays CHART-FAMILY ONLY, unwidened: a dimensionless
metric/table is what those families are for.

The id decision: kept chart-measures-missing, added widget-measures-missing

Measured first, as the ruling required. "chart" does not name the condition once the
population is every family — a metric tile is not a chart — so the new families needed a
family-neutral id. Whether the old id could be RETIRED turned on whether anything outside
the rule file depends on the string. git grep -n "chart-measures-missing" -- packages content docs skills
on main (7087f99c1) returns four hits, two of them outside validate-widget-bindings.*:

.changeset/chart-empty-selection-rules.md:11:- `chart-measures-missing` — a chart-family widget selects no measures (`values` empty or
packages/lint/src/index.ts:25:  CHART_MEASURES_MISSING,
packages/lint/src/validate-widget-bindings.ts:92: * - `chart-measures-missing` — a chart-family widget selects NO measures
packages/lint/src/validate-widget-bindings.test.ts:1524:describe('chart-measures-missing / chart-dimensions-missing (#15462)', () => {

packages/lint/src/index.ts is the package's published barrel — and
rule-id-barrel-exports.test.ts makes that reachability a public-surface contract, not a
convenience. .changeset/chart-empty-selection-rules.md is the release-note record of the
PR that added the id. Renaming would have retired a reachable id and falsified an
already-merged PR's changeset; so the ruling's other branch applies:

  • chart-measures-missing is untouched — same id, same chart-family population, same
    message ("no chart is drawn at all"), same suppression. A board that already wrote
    suppressWarnings: ['chart-measures-missing'] keeps working, pinned by a test.
  • widget-measures-missing is new, for the non-chart declared families, with the same
    message shape and the consequence each family actually has: the single-value families get
    "the single KPI number this tile is for is not drawn at all", table/pivot get "no
    table is rendered at all"
    . Hint keeps "select at least one measure BY NAME (declared
    measures: …)" plus the suppression tail; it does not steer toward a dimension (that
    clause is chart-only and would be false advice on a metric).

One id could only have produced the per-family messages by branching internally anyway,
so the split costs nothing in message quality and keeps every reference true.

Population is derived, never hand-listed: NON_CHART_DATASET_WIDGET_TYPES is
ChartTypeSchema.options minus CHART_FAMILY_WIDGET_TYPES, so the three sets stay a
partition of the taxonomy by construction and a family added to the taxonomy or to either
exception set lands on exactly one side without a second edit. A widget type outside the
taxonomy is still judged by none of the three ids.

Mutual exclusion and ordering are the pin's own, and unchanged: the measures check is
evaluated BEFORE chart-dimensions-missing (now as the control flow itself —
if (values.length === 0) { … } else if (dims.length === 0) { … }), and rule (e)
table-count-only still continues on values.length === 0. Both non-overlaps are pinned
by "exactly one finding" tests.

Files

File Why
packages/lint/src/validate-widget-bindings.ts new id + derived population + emission arm; docblock rule list and the "two empty-selection shapes" section updated with a (#15508) paragraph
packages/lint/src/validate-widget-bindings.test.ts new #15508 block, the acceptance battery, and the three #15462 population pins re-stated for the widened shape
packages/lint/src/index.ts one added barrel line. Not in the dispatch's hot-file fence, but not optional either: rule-id-barrel-exports.test.ts fails any exported rule id that no published barrel re-exports. Additive — no existing export renamed or removed.
.changeset/widget-measures-missing-every-family.md @objectstack/lint: minor — a new exported id constant is additive API

Population pins re-stated (they asserted the OLD, chart-family-only population, and are
where the widening is visible): a single-value or tabular family with no measures is NOT the measures finding now asserts the family-neutral id fires and the chart id does not;
every chart family reports, and no other family does became every declared family reports the MEASURES shape; only the chart family reports the dimensions one, sweeping the
whole taxonomy and asserting each type reports exactly one of the two measures ids. The
CHART_FAMILY_WIDGET_TYPES ⊆ ChartTypeSchema / exception-set-mirror pins are untouched and
still green; the new block adds the partition assertion for the other side.

Verification

Exit codes captured BEFORE any pipe (cmd > file 2>&1; EXIT=$?). Heavy runs serialised
through OS_VERIFY_LOCK_SLOT=issue-15508 scripts/pm/os-verify-lock.sh. Head at every run
below: 4712be5d8.

Build (dependency closure — the tests import @objectstack/spec/ui through its exports):

pnpm --filter '@objectstack/lint...' build
os-verify-lock: VERDICT command-exit 0 · held the lock 170s (2m50s) · waited 0s
packages/lint build: check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.

Tests:

pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-widget-bindings.test.ts src/rule-id-barrel-exports.test.ts
 Test Files  2 passed (2)
      Tests  153 passed (153)
os-verify-lock: VERDICT command-exit 0 · held the lock 9s · waited 0s

pnpm --filter @objectstack/lint test
 Test Files  96 passed (96)
      Tests  3023 passed (3023)
os-verify-lock: VERDICT command-exit 0 · held the lock 45s · waited 0s

pnpm --filter @objectstack/lint typecheck
check:test-typecheck: OK — @objectstack/lint's test layer compiles under packages/lint/tsconfig.test.json
os-verify-lock: VERDICT command-exit 0 · held the lock 16s · waited 0s

Ablation — the population widening, reverted

Trap-guarded (trap 'git -C "$REPO_ROOT" checkout HEAD -- "$TARGET"' EXIT INT TERM,
absolute paths, run from a COMMITTED implementation so the restore leg has a real
reference point). No dist rebuild is involved and none is owed: the test imports the
rule through the relative specifier ./validate-widget-bindings.js, i.e. src/, and
packages/lint/vitest.config.ts sets one unrelated option (disableConsoleIntercept) and
no alias — so the mutation is the code under measurement. Mutation: the emission's
} else if (isNonChartDatasetWidgetType(w.type)) { replaced by a false arm, i.e. the
chart-family-only population restored.

Mutation proven on disk by grep counts in BOTH directions plus the blob:

HEAD_BLOB=5ef6b84a45f25606332b73b7b19ee0ec8a48390f
BEFORE: original-predicate-lines=1 injected-lines=0
AFTER : original-predicate-lines=0 injected-lines=1
MUTATED_BLOB=2b39230da47fc9daa88cfbd6bfcfc0f51970db4c (differs from HEAD: yes)

Result — RED, in the predicted direction (the new families' cases fail; the chart-family
cases stay green, which is the arm the mutation does not touch):

 Test Files  1 failed (1)
      Tests  11 failed | 138 passed (149)
os-verify-lock: VERDICT command-exit 1 · held the lock 9s · waited 0s

 FAIL … widget-measures-missing (#15508) > warns when a `metric` tile selects no measures, naming the KPI consequence
AssertionError: expected [] to have a length of 1 but got +0
 FAIL … widget-measures-missing (#15508) > warns when a `table` selects no measures, naming the TABLE consequence
 FAIL … widget-measures-missing (#15508) > a `metric` with NO measures and NO dimensions reports exactly one finding
 FAIL … widget-measures-missing (#15508) > a `table` with NO measures and NO dimensions reports once — not also table-count-only
 FAIL … [#15508] a single-value or tabular family with no measures reports the FAMILY-NEUTRAL id
 (11 failures total, every one naming a metric or tabular family)

Restore proven by state, not by exit code:

diff-empty=yes                    (git diff HEAD -- PATH)
worktree_blob=5ef6b84a45f25606332b73b7b19ee0ec8a48390f
head_blob=5ef6b84a45f25606332b73b7b19ee0ec8a48390f
injected-marker-count=0   original-predicate-lines=1

Green again on the restored tree:

 Test Files  1 passed (1)
      Tests  149 passed (149)
os-verify-lock: VERDICT command-exit 0 · held the lock 10s · waited 0s

Derived gate family

node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack
(exit 0) at 4712be5d8, over its own change set (4 paths, three-dot vs merge base
7087f99c1) — 52 commands. All 52 run; 50 exit 0, 2 exit 3 (NOT MEASURED, below). Verdict
lines from the two the dispatch named:

check-nul-bytes: OK (scanned 7577 text file(s) -- 7577 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).
✓ dispatch-gates self-test: 1445 cases pass.

Changeset family, all exit 0: check-changeset-no-major --base origin/main,
check-empty-changeset --base origin/main, check:changeset-gate-self-tests,
check:objectui-changeset, and check-changeset-fixed (run explicitly — the derivation
marks its roster ⛔ because it lives under .changeset, one of my paths). The other three
⛔-marked roster gates ran too: check:authz-resolver, check:error-code-casing,
check:filter-alias-parity — exit 0.

NOT MEASURED (named, counted as neither green nor red):

  • pnpm check:dual-build-cjs-loads — exit 3, PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ (74+ packages). Needs a whole-repo build;
    CI does it.
  • pnpm check:type-check-debt — exit 3, check-type-check-coverage: PREREQUISITE NOT MET … --re-measure cannot run: 29 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk. Same cause. (Its sibling check:type-check-coverage
    ran and is green.)
  • The 7 families whose argv takes a value from the workflow ($RUNNER_TEMP,
    matrix.shard) — the derivation prints them as ⊘ NOT MEASURED itself; they have no
    value outside a CI run.
  • The always-runs tail (workflows with no path filter) is CI's, not derived here.

Fixture radius

Swept every fixture that could feed the widened rule: repo-wide scan for a dataset-bound
widget of a non-chart family with no values, plus every caller of
validateWidgetBindings (metadata-protocol publish-gate and drafts-closure boards,
objectql protocol-meta, cli, and the lint suites). Three candidates, none needing a change:
packages/cli/test/authoring-rule-command-parity.test.ts filters to severity === 'error'
so a new warning cannot move it; packages/lint/src/validate-filter-tokens.test.ts and
packages/sdui-parser/** do not run this rule. Every shipped board
(system_overview, the showcase/CRM/todo examples) selects its measures, so first-party
metadata stays clean — pinned by the existing system_overview case and by a new one that
reports a measureless CLONE of that tile.

Not flipped to ready, no auto-merge, not merged — the PM does that.

🤖 Generated with Claude Code

https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk


Generated by Claude Code

…not only charts

The pinned `@object-ui` renderer's `values.length === 0` return
(`packages/plugin-dashboard/src/DatasetWidget.tsx:683` at `.objectui-sha` =
`a472b07167a39e55491109e864bb5a54027dcfbd`) is type-independent and stands above
`isMetric` (`:423`, `METRIC_TYPES` at `:343`), `isTable` (`:424`) and the chart
branch alike. A `metric`, `kpi`, `gauge`, `solid-gauge`, `bullet`, `table` or
`pivot` widget that selects no measures therefore renders the same authoring
placeholder — the KPI number or the table is not drawn at all — and nothing
reported it: `chart-measures-missing` was chart-family only, `table-count-only`
requires `values.length > 0`, and the rules that iterate `dimensions[]`/`values[]`
are silent on an empty array by construction.

Adds `widget-measures-missing` for the non-chart declared families, with the same
warning tier, the same per-widget `suppressWarnings` suppression and a message
that states the consequence its family actually has. `chart-measures-missing` is
left exactly as ruled — same id, same population, same wording — because it is
reachable from the package barrel (a public-surface contract) and may already be
written into a board's `suppressWarnings`. The dimensions arm stays chart-family
only: a dimensionless `metric`/`table` is what those families are for.

The population is derived, never hand-listed: `NON_CHART_DATASET_WIDGET_TYPES` is
the `ChartTypeSchema` taxonomy minus `CHART_FAMILY_WIDGET_TYPES`, so the three
sets stay a partition by construction. Mutual exclusion is the pin's own order —
the measures check runs before the dimensions one, and rule (e) already skips an
empty selection — pinned by tests that assert exactly one finding per widget.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
@github-actions github-actions Bot added the size/m label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 4 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/lint/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via validateWidgetBindings (symbol, a top-level function))
  • content/docs/releases/v17.mdx (via validateWidgetBindings (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/lint/src/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fa125f3bfe434a615c846c0ed0e1ab01de386a17packageMentionDocs.

Which tree this was computed on

This run read content/docs from 682b7e563a9413b3ecfcdbce8ba7857014ec3309 — the merge of head 4712be5d85f7d6fae7284051c8e8a7b3a5f69226 into base fa125f3bfe434a615c846c0ed0e1ab01de386a17, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 682b7e563a9413b3ecfcdbce8ba7857014ec3309 && git checkout 682b7e563a9413b3ecfcdbce8ba7857014ec3309
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa125f3bfe434a615c846c0ed0e1ab01de386a17 4712be5d85f7d6fae7284051c8e8a7b3a5f69226 && git checkout -B drift-repro fa125f3bfe434a615c846c0ed0e1ab01de386a17 && git merge --no-ff 4712be5d85f7d6fae7284051c8e8a7b3a5f69226

node scripts/docs-audit/affected-docs.mjs --json fa125f3bfe434a615c846c0ed0e1ab01de386a17

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fa125f3bfe434a615c846c0ed0e1ab01de386a17 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM seat domain:devx @ objectstack (#6023), session session_012zGPuVVX3deAx9LdjK8jCk.

Reviewed against the three-dot diff at 4712be5d8 (4 files, +348/−16), not the report. Closing keyword checked: first line Fixes #15508 and no other card number adjacent to a keyword.

What the diff does, checked line by line

  • packages/lint/src/validate-widget-bindings.ts: the (d1) arm is re-ordered so values.length === 0 is the OUTER test (the pin's DatasetWidget.tsx:683 order, cited at a472b0716), then family: chart family → chart-measures-missing unchanged (id, message, hint, suppression); non-chart declared types → new widget-measures-missing, message stating the family's own consequence (KPI number not drawn / no table rendered), same warning tier and per-widget suppressWarnings. chart-dimensions-missing stays chart-family only. NON_CHART_DATASET_WIDGET_TYPES is DERIVED as ChartTypeSchema.options − CHART_FAMILY_WIDGET_TYPES (a partition by construction, pinned by a test), and METRIC_WIDGET_TYPES/TABULAR_WIDGET_TYPES (:519/:529) are the existing renderer-set copies. Docblock updated with a "(A dashboard widget of ANY non-chart family that selects no measures renders the authoring placeholder, and no lint rule reports it #15508)" paragraph; the id decision is argued there: the old id is reachable from the published barrel and written into an already-merged changeset, so it was kept and the population split, not renamed — I verified the two references on origin/main (packages/lint/src/index.ts, .changeset/chart-empty-selection-rules.md).
  • packages/lint/src/index.ts: ONE added barrel line — outside the dispatch fence, required by rule-id-barrel-exports.test.ts (every exported rule id must be re-exported), declared in the PR body. Accepted on the four conditions (same defect class, mechanical, no other claim, same gate family).
  • Tests (+190): family-neutral id per family, the bar control unchanged, absent values key ≡ empty array, suppression independence between the two ids, exactly-one-finding for a measureless+dimensionless metric AND table (never also table-count-only), partition pin, a measureless clone of a shipped system_overview tile, and the "advises, never gates" battery extended to the new id.
  • .changeset/widget-measures-missing-every-family.md: @objectstack/lint: minor (new exported id = additive API), body names both ids and the pin lines.

Measured / arbiter: packages/lint imports typescript (absent in the PM checkout) — the suite is NOT MEASURED here; the dev's readings at this head: 149/149 in the rule file, 3023/3023 package-wide, typecheck OK, ablation red in the predicted direction (11 cases naming metric/tabular families, chart cases green), restore blob-proven. CI (Lint & Repo Gates, Type Check) is the arbiter — in progress at review time; flip waits for both required jobs.

Fences held: content/docs/releases/ untouched; governed-surface test check-governed-merges.mjs --test on the four paths → 0 of 4 path(s) hit the register; no other open PR holds these files; git merge-tree origin/main clean. #15575 (the chart-measure-unknown message card) is next on validate-widget-bindings.ts and waits for this to land.

Flipping ready + enabling auto-merge once the two required jobs read success. Fixes #15508 closes on merge.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A dashboard widget of ANY non-chart family that selects no measures renders the authoring placeholder, and no lint rule reports it

2 participants